#include "dicom/upload/hash_cache.h"
#include "dicom/storage/decrypt.h"
#include <gwbase/base/logging.h>

namespace dicom {

const char* kCacheDbFileName = "hash_cache.db";

bool HashEntry::IsComplete() const {
  if (src_md5.size() != 32) {
    return false;
  }
  if (src_size < 0) {
    return false;
  }
  if (src_digest.size() < 32 + 40 + 1) {  // MD5+SHA1+SIZE
    return false;
  }
  if (src_sha512.size() != 128) {
    return false;
  }
  if (hexkey.size() != 64) {
    return false;
  }
  if (enc_size < 0) {
    return false;
  }
  if (enc_digest.size() != 40) {
    return false;
  }
  return true;
}

HashCache::HashCache() {}
HashCache::~HashCache() {}

Status HashCache::Open(const std::string& path) {
  Status s;
  s = dicom::db::Database::Open(path, &db_);
  if (!s.ok()) {
    LOG_ERROR << "failed to create hash cache database";
    return s;
  }

  s = CreateTable();
  if (!s.ok()) {
    LOG_ERROR << "failed to create table in hash cache database";
  }

  return s;
}

bool HashCache::UpdateHashEntry(const HashEntry* entry) {
  dicom::LockGuard g(mu_);
  HashEntry old;
  if (GetHashEntryWithLock(entry->src_md5, entry->hexkey, &old)) {
    // When file is downloaded from cloud, we only have src_md5 and
    // hexkey, other fields are emtpy. Now, we have an opportunity to
    // fill these fields
    if (old.status == HASH_DOWNLOADED && entry->status != HASH_DOWNLOADED) {
      assert(old.src_md5 == entry->src_md5);
      old.src_size = entry->src_size;
      old.src_digest = entry->src_digest;
      old.src_sha512 = entry->src_sha512;
      assert(old.hexkey == entry->hexkey);
      old.enc_size = entry->enc_size;
      old.enc_digest = entry->enc_digest;
      InsertHashEntry(&old);
    }

    if (old.status >= entry->status) {
      return true;
    }

    if (entry->status == HASH_DOWNLOADED) {
      old.status = HASH_DOWNLOADED;
      return InsertHashEntry(&old);
    }
  }

  return InsertHashEntry(entry);
}

bool HashCache::GetHashEntry(const std::string& src_digest,
                             const std::string& hexkey, HashEntry* result) {
  LockGuard g(mu_);
  return GetHashEntryWithLock(src_digest, hexkey, result);
}

bool HashCache::InsertHashEntry(const HashEntry* entry) {
  const char* sql =
      "replace into hash_cache_tab "
      "(src_md5, src_size, src_digest, src_sha512, "
      "hexkey, enc_size, enc_digest, status) "
      "values (?, ?, ?, ?, ?, ?, ?, ?)";

  dicom::db::StmtPtr stmt = db_->Prepare(sql);
  stmt->BindText(entry->src_md5);
  stmt->BindInt64(entry->src_size);
  stmt->BindText(entry->src_digest);
  stmt->BindText(entry->src_sha512);
  stmt->BindText(entry->hexkey);
  stmt->BindInt64(entry->enc_size);
  stmt->BindText(entry->enc_digest);
  stmt->BindInt32(static_cast<int32_t>(entry->status));
  stmt->Step();
  return true;
}

bool HashCache::GetHashEntryWithLock(const std::string& src_md5,
                                     const std::string& hexkey,
                                     HashEntry* result) {
  const char* sql =
      "select src_md5, src_size, src_digest, "
      "src_sha512, hexkey, enc_size, "
      "enc_digest, status from hash_cache_tab where "
      " src_md5 = ? and hexkey = ? ";

  dicom::db::StmtPtr stmt = db_->Prepare(sql);
  stmt->BindText(src_md5);
  stmt->BindText(hexkey);
  stmt->Step();
  if (!stmt->eof()) {
    return LoadHashEntry(stmt, result);
  }
  return false;
}

bool HashCache::LoadHashEntry(dicom::db::StmtPtr stmt, HashEntry* entry) {
  int pos = 0;
  entry->src_md5 = stmt->GetText(pos++);
  entry->src_size = stmt->GetInt64(pos++);
  entry->src_digest = stmt->GetText(pos++);
  entry->src_sha512 = stmt->GetText(pos++);
  entry->hexkey = stmt->GetText(pos++);
  entry->enc_size = stmt->GetInt64(pos++);
  entry->enc_digest = stmt->GetText(pos++);
  entry->status = static_cast<HashEntryStatus>(stmt->GetInt32(pos++));

  return true;
}

bool HashCache::GetBestEntry(const std::string& src_md5, HashEntry* result) {
  const char* sql =
      "select src_md5, src_size, src_digest, src_sha512, "
      "hexkey, enc_size, enc_digest, status from hash_cache_tab "
      "where src_md5 = ? order by status DESC limit 1";

  LockGuard g(mu_);
  db::StmtPtr stmt = db_->Prepare(sql);
  stmt->BindText(src_md5);
  stmt->Step();
  if (stmt->eof()) {
    return false;
  }

  return LoadHashEntry(stmt, result);
}

Status HashCache::CreateTable() {
  Status s;
  const char* create_tab =
      "create table if not exists hash_cache_tab("
      "src_md5 varchar, "
      "src_size integer, "
      "src_digest varchar, "
      "src_sha512 varchar, "
      "hexkey varchar, "
      "enc_size integer, "
      "enc_digest varchar, "
      "status int, "
      "primary key (src_md5, hexkey)"
      ")";
  db_->RunSql(create_tab);

  const char* create_idx =
      "create index if not exists src_digest_idx "
      "on hash_cache_tab(src_md5)";
  db_->RunSql(create_idx);

  return s;
}

HashCache* OpenHashCache(const DicomConf* conf) {
  std::string dbdir = conf->GetDbDir();
  std::string path = path::Join(dbdir, kCacheDbFileName);

  HashCache* c = new HashCache();
  Status s = c->Open(path);
  if (!s.ok()) {
    delete c;
    return NULL;
  }

  return c;
}

bool HashEntryEqual(const HashEntry& h1, const HashEntry& h2) {
  if (h1.src_md5 != h2.src_md5) {
    LOG_INFO << "src_md5: " << h1.src_md5 << " : " << h2.src_md5;
    return false;
  }
  if (h1.src_size != h2.src_size) {
    LOG_INFO << "src_size: " << h1.src_size << " : " << h2.src_size;
    return false;
  }
  if (h1.src_digest != h2.src_digest) {
    LOG_INFO << "src_digest: " << h1.src_digest << " : " << h2.src_digest;
    return false;
  }
  if (h1.src_sha512 != h2.src_sha512) {
    LOG_INFO << "src_sha512: " << h1.src_sha512 << " : " << h2.src_sha512;
    return false;
  }

  if (h1.hexkey != h2.hexkey) {
    LOG_INFO << "hexkey: " << h1.hexkey << " : " << h2.hexkey;
    return false;
  }
  if (h1.enc_size != h2.enc_size) {
    LOG_INFO << "enc_size: " << h1.enc_size << " : " << h2.enc_size;
    return false;
  }
  if (h1.enc_digest != h2.enc_digest) {
    LOG_INFO << "enc_digest: " << h1.enc_digest << " : " << h2.enc_digest;
    return false;
  }
  if (h1.status != h2.status) {
    LOG_INFO << "status: " << h1.status << " : " << h2.status;
    return false;
  }

  return true;
}

Status GetEncrytFileHash(HashCache* hcache, const std::string& path,
                         const std::string& src_md5, const std::string& hexkey,
                         HashEntry* hash) {
  assert(src_md5.size() == 32);
  assert(hexkey.size() == 64);
  hash->src_md5 = src_md5;
  hash->hexkey = hexkey;

  Status s;
  bool ok = hcache->GetHashEntry(src_md5, hexkey, hash);
  if (ok) {
    if (hash->src_size >= 0 && !hash->src_digest.empty() &&
        !hash->src_sha512.empty() && !hash->enc_digest.empty() &&
        hash->enc_size >= 0) {
      return Status();
    }
  }

  // LOG_INFO << "not in cache, do decryption";

  // OK, we must caculate this from encrypted file
  NullWriter w;
  FileReader r;
  s = r.Open(path);
  if (!s.ok()) {
    return s;
  }

  Decryptor d;
  s = d.DoDecryptAndHash(hexkey, &r, &w, hash);
  if (!s.ok()) {
    return s;
  }

  assert(hash->src_size >= 0 && !hash->src_digest.empty() &&
         !hash->src_sha512.empty() && !hash->enc_digest.empty() &&
         hash->enc_size >= 0);

  hash->status = HASH_LOCAL;

  hcache->UpdateHashEntry(hash);
  return s;
}
}
